#!/usr/bin/env python
# -*- coding= utf-8 -*-

"""pySlip demonstration program."""


import tkinter_error
try:
    import wx
except ImportError:
    msg = 'Sorry, you must install wxPython'
    tkinter_error.tkinter_error(msg)

# If we have log.py, well and good.  Otherwise ...
try:
    import log
    log = log.Log('pyslip.log', log.Log.DEBUG)
except ImportError:
    def log(*args, **kwargs):
        pass

import os
import threading
import urllib2
import Queue
import math
import pyslip
from tiles import Tiles
from wx.lib.embeddedimage import PyEmbeddedImage


######
# Various demo constants
######

# demo name/version
DemoName = "pySlip %s - 'Net Demonstration" % pyslip.__version__
DemoVersion = '2.0'

# tiles info
TileDirectory = 'tiles_osm'
MinTileLevel = 0

# initial view level and position
#InitViewLevel = 4
#InitViewPosition = (103.770, 1.335)
InitViewLevel = 6
InitViewPosition = (-8.0, 36.63333)

# levels on which various layers show
MRPointShowLevels = range(17)
MRImageShowLevels = range(17)
MRTextShowLevels = range(17)
MRPolyShowLevels = range(17)

# the number of decimal places in a lon/lat display
LonLatPrecision = 3

# startup size of the application
DefaultAppSize = (800, 665)

# how close click has to be before point is selected
# the value is distance squared (degrees^2)
PointSelectDelta = 0.025

# unselected point colour (rgb) and size
PointsColour = '#ff0000'
PointsSize = 3

# Selected point colour (rgb) and size
SelectColour = '#0000ff'
SelectSize = 5

# Polygon point colour (rgba) and size
PolygonColour = '#0000ff'
PolygonSize = 4

# Polygon2 point colour (rgba) and size
Polygon2Colour = '#000000'
Polygon2Size = 4

# image used for shipwrecks, glassy buttons, etc
ShipImg = 'graphics/shipwreck.png'

GlassyImg2 = 'graphics/glassy_button_2.png'
SelGlassyImg2 = 'graphics/selected_glassy_button_2.png'
GlassyImg3 = 'graphics/glassy_button_3.png'
SelGlassyImg3 = 'graphics/selected_glassy_button_3.png'
GlassyImg4 = 'graphics/glassy_button_4.png'
SelGlassyImg4 = 'graphics/selected_glassy_button_4.png'
GlassyImg5 = 'graphics/glassy_button_5.png'
SelGlassyImg5 = 'graphics/selected_glassy_button_5.png'
GlassyImg6 = 'graphics/glassy_button_6.png'
SelGlassyImg6 = 'graphics/selected_glassy_button_6.png'

# image used for shipwrecks
CompassRoseGraphic = 'graphics/compass_rose.png'

######
# Various GUI layout constants
######

# sizes of various spacers
HSpacerSize = (3,1)         # horizontal in application screen
VSpacerSize = (1,5)         # vertical in control pane

# border width when packing GUI elements
PackBorder = 1


################################################################################
# Override the wx.TextCtrl class to add read-only style and background colour
################################################################################

# background colour for the 'read-only' text field
ControlReadonlyColour = '#ffffcc'

class ROTextCtrl(wx.TextCtrl):
    """Override the wx.TextCtrl widget to get read-only text control which
    has a distinctive background colour."""

    def __init__(self, parent, value, tooltip='', *args, **kwargs):
        wx.TextCtrl.__init__(self, parent, wx.ID_ANY, value=value,
                             style=wx.TE_READONLY, *args, **kwargs)
        self.SetBackgroundColour(ControlReadonlyColour)
        self.SetToolTip(wx.ToolTip(tooltip))

################################################################################
# Override the wx.StaticBox class to show our style
################################################################################

class AppStaticBox(wx.StaticBox):

    def __init__(self, parent, label, *args, **kwargs):
        if label:
            label = '  ' + label + '  '
        wx.StaticBox.__init__(self, parent, wx.ID_ANY, label, *args, **kwargs)

################################################################################
# Class for a LayerControl widget.
################################################################################

myEVT_ONOFF = wx.NewEventType()
myEVT_SHOWONOFF = wx.NewEventType()
myEVT_SELECTONOFF = wx.NewEventType()

EVT_ONOFF = wx.PyEventBinder(myEVT_ONOFF, 1)
EVT_SHOWONOFF = wx.PyEventBinder(myEVT_SHOWONOFF, 1)
EVT_SELECTONOFF = wx.PyEventBinder(myEVT_SELECTONOFF, 1)

class LayerControlEvent(wx.PyCommandEvent):
    """Event sent when a LayerControl is changed."""

    def __init__(self, eventType, id):
        wx.PyCommandEvent.__init__(self, eventType, id)

class LayerControl(wx.Panel):

    def __init__(self, parent, title, selectable=False, editable=False,
                 **kwargs):
        """Initialise a LayerControl instance.

        parent      reference to parent object
        title       text to ahow in static box outline
        selectable  True if 'selectable' checkbox is to be displayed
        editable    True if layer can be edited
        **kwargs    keyword args for Panel
        """

        # create and initialise the base panel
        wx.Panel.__init__(self, parent=parent, id=wx.ID_ANY, **kwargs)
        self.SetBackgroundColour(wx.WHITE)

        self.selectable = selectable
        self.editable = editable

        box = AppStaticBox(self, title)
        sbs = wx.StaticBoxSizer(box, orient=wx.VERTICAL)
        gbs = wx.GridBagSizer()

        self.cbx_onoff = wx.CheckBox(self, wx.ID_ANY, label='Add layer')
        gbs.Add(self.cbx_onoff, (0,0), span=(1,4))

        self.cbx_show = wx.CheckBox(self, wx.ID_ANY, label='Show')
        gbs.Add(self.cbx_show, (1,1))
        self.cbx_show.Disable()

        if selectable:
            self.cbx_select = wx.CheckBox(self, wx.ID_ANY, label='Select')
            gbs.Add(self.cbx_select, (1,2))
            self.cbx_select.Disable()

        if editable:
            self.cbx_edit = wx.CheckBox(self, wx.ID_ANY, label='Edit')
            gbs.Add(self.cbx_edit, (1,3))
            self.cbx_edit.Disable()

        sbs.Add(gbs)
        self.SetSizer(sbs)
        sbs.Fit(self)

        # tie handlers to change events
        self.cbx_onoff.Bind(wx.EVT_CHECKBOX, self.onChangeOnOff)
        self.cbx_show.Bind(wx.EVT_CHECKBOX, self.onChangeShowOnOff)
        if selectable:
            self.cbx_select.Bind(wx.EVT_CHECKBOX, self.onChangeSelectOnOff)
#        if editable:
#            self.cbx_edit.Bind(wx.EVT_CHECKBOX, self.onChangeEditOnOff)

    def onChangeOnOff(self, event):
        """Main checkbox changed."""

        event = LayerControlEvent(myEVT_ONOFF, self.GetId())
        event.state = self.cbx_onoff.IsChecked()
        self.GetEventHandler().ProcessEvent(event)

        if self.cbx_onoff.IsChecked():
            self.cbx_show.Enable()
            self.cbx_show.SetValue(True)
            if self.selectable:
                self.cbx_select.Enable()
                self.cbx_select.SetValue(False)
            if self.editable:
                self.cbx_edit.Enable()
                self.cbx_edit.SetValue(False)
        else:
            self.cbx_show.Disable()
            if self.selectable:
                self.cbx_select.Disable()
            if self.editable:
                self.cbx_edit.Disable()

    def onChangeShowOnOff(self, event):
        """Show checkbox changed."""

        event = LayerControlEvent(myEVT_SHOWONOFF, self.GetId())
        event.state = self.cbx_show.IsChecked()
        self.GetEventHandler().ProcessEvent(event)

    def onChangeSelectOnOff(self, event):
        """Select checkbox changed."""

        event = LayerControlEvent(myEVT_SELECTONOFF, self.GetId())
        if self.selectable:
            event.state = self.cbx_select.IsChecked()
        else:
            event_state = False
        self.GetEventHandler().ProcessEvent(event)

################################################################################
# Make a Tile object for MapQuest tiles
################################################################################

class MyTiles(Tiles):
    """Override the Tiles base class."""

    TileSize = 256      # width/height of tiles

    # the pool of tile servers used and tile path on server
    # to each tile, %params are (level, x, y)
# OSM tiles
    TileServers = ['http://otile1.mqcdn.com',
                   'http://otile2.mqcdn.com',
                   'http://otile3.mqcdn.com',
                   'http://otile4.mqcdn.com']
    TileURLPath = '/tiles/1.0.0/osm/%d/%d/%d.jpg'
# satellite tiles
#    TileServers = ['http://oatile1.mqcdn.com',
#                   'http://oatile2.mqcdn.com',
#                   'http://oatile3.mqcdn.com',
#                   'http://oatile4.mqcdn.com']
#    TileURLPath = '/tiles/1.0.0/sat/%d/%d/%d.jpg'

    # maximum pending requests for each tile server
    MaxServerRequests = 2

    # available tile levels in MQ_OSM [0, ..., 16]
    # note: some tiles in levels 13+ don't exist
# OSM tiles
    TileLevels = range(17)
# satellite tiles
#    TileLevels = range(13)         # [0, ..., 12] for the satellite tiles

    # path to tile in on-disk cache, %params are (x, y)
    TilePath = os.path.join('%d', '%d.jpg')

    # a pale green 256x256 tile for the 'pending' tile
    PendingImage = (
      "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAAAAXNSR0IArs4c6QAAAAlwSFlz"
      "AAALEwAACxMBAJqcGAAAAAd0SU1FB9wDDgM1MxHwZu4AAAAZdEVYdENvbW1lbnQAQ3JlYXRl"
      "ZCB3aXRoIEdJTVBXgQ4XAAAB/UlEQVR42u3TQREAAAQAQfQPqwFvGexGuJnLng74qiTAAGAA"
      "MAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgAD"
      "gAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAGAAOAAcAAYAAwABgADAAG"
      "AAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAYAAw"
      "ABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAGAAMAAYAA4ABwABgADAAGAAMAAYAA4ABwABg"
      "ADAAGAAMAAYAA4ABwABgADAAGAAMAAYAA4ABwABgADAAGAAMAAYAA4ABwABgADAAGAAMAAYA"
      "A4ABwABgADAAGAAMAAbAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwA"
      "BgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAA"
      "MAAYAAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAA"
      "YAAwABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADAAGgGsBiN4E31uv8wcAAAAA"
      "SUVORK5CYII=")

    # a pale red 256x256 tile for the 'error' tile
    ErrorImage = (
      "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAIAAADTED8xAAAAAXNSR0IArs4c6QAAAAlwSFlz"
      "AAALEwAACxMBAJqcGAAAAAd0SU1FB9wDDgM1KJuVrwIAAAAZdEVYdENvbW1lbnQAQ3JlYXRl"
      "ZCB3aXRoIEdJTVBXgQ4XAAAB/UlEQVR42u3TQREAAAQAQfQPqwFvGexGuJnL6Q74qiTAAGAA"
      "MAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgAD"
      "gAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAGAAOAAcAAYAAwABgADAAG"
      "AAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAYAAw"
      "ABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAGAAMAAYAA4ABwABgADAAGAAMAAYAA4ABwABg"
      "ADAAGAAMAAYAA4ABwABgADAAGAAMAAYAA4ABwABgADAAGAAMAAYAA4ABwABgADAAGAAMAAYA"
      "A4ABwABgADAAGAAMAAbAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwA"
      "BgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAAMAAYAAwABgADgAHAAGAA"
      "MAAYAAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAA"
      "YAAwABgADAAGAAOAAcAAYAAwABgADAAGAAOAAcAAYAAwABgADAAGgGsBiO0E3x03cI8AAAAA"
      "SUVORK5CYII=")


    def __init__(self, tile_cache_dir, tile_levels, callback=None,
                 http_proxy=None, pending_file=None, error_file=None):
        """Override the base class for local tiles.

        tile_cache_dir  tile cache directory, may contain tiles
        tile_levels     list of tile levels to be served
        callback        caller function to call on tile available
        http_proxy      HTTP proxy to use if there is a firewall
        pending_file    path to picture file for the 'pending' tile
        error_file      path to picture file for the 'error' tile
        """

        # first, initialize with base code
        Tiles.__init__(self, tile_cache_dir, tile_levels)

        # save the CallAfter() function
        self.callback = callback

        # tiles extent for OSM tile data (left, right, top, bottom)
        self.extent = (-180.0, 180.0, -85.0511, 85.0511)

        # prepare tile cache if not already there
        self.tile_cache_dir = tile_cache_dir
        if not os.path.isdir(tile_cache_dir):
            if os.path.isfile(tile_cache_dir):
                msg = ("%s doesn't appear to be a tile cache directory"
                       % tile_cache_dir)
                raise Exception(msg)
            os.makedirs(tile_cache_dir)
        for level in self.TileLevels:
            level_dir = os.path.join(tile_cache_dir, '%d' % level)
            if not os.path.isdir(level_dir):
                os.makedirs(level_dir)

        # set the list of queued unsatisfied requests to 'empty'
        self.queued_requests = {}

        # OSM tiles always (256, 256)
        self.tile_size_x = self.TileSize
        self.tile_size_y = self.TileSize

        # prepare the "pending" and "error" images
        if pending_file:
            self.pending_tile_image = wx.Image(pending_file, wx.BITMAP_TYPE_ANY)
            self.pending_tile = self.pending_tile_image.ConvertToBitmap()
        else:
            self.pending_tile_image = PyEmbeddedImage(self.PendingImage)
            self.pending_tile = self.pending_tile_image.GetBitmap()

        if error_file:
            self.error_tile_image = wx.Image(error_file, wx.BITMAP_TYPE_ANY)
            self.error_tile = self.error_tile_image.ConvertToBitmap()
        else:
            self.error_tile_image = PyEmbeddedImage(self.ErrorImage)
            self.error_tile = self.error_tile_image.GetBitmap()

        # test for firewall - use proxy (if supplied)
        test_url = self.TileServers[0] + self.TileURLPath % (0, 0, 0)
        try:
            urllib2.urlopen(test_url)
        except:
            log('Error doing simple connection to: %s' % test_url)
            log(''.join(traceback.format_exc()))

            if http_proxy:
                log('Try using proxy: %s' % str(http_proxy))
                proxy = urllib2.ProxyHandler({'http': http_proxy})
                opener = urllib2.build_opener(proxy)
                urllib2.install_opener(opener)
                try:
                    urllib2.urlopen(test_url)
                except:
                    msg = ("Using HTTP proxy %s, "
                           "but still can't get through a firewall!")
                    raise Exception(msg)
            else:
                msg = ("There is a firewall but you didn't "
                       "give me an HTTP proxy to get through it?")
                raise Exception(msg)

        # set up the request queue and worker threads
        self.request_queue = Queue.Queue()  # entries are (level, x, y)
        self.workers = []
        for server in self.TileServers:
            for num_threads in range(self.MaxServerRequests):
                worker = TileWorker(server, self.TileURLPath,
                                    self.request_queue, self._tile_available,
                                    self.error_tile_image)
                self.workers.append(worker)
                worker.start()

    def GetInfo(self, level):
        """Get tile info for a particular level.

        level  the level to get tile info for

        Returns (num_tiles_x, num_tiles_y, ppd_x, ppd_y).
        """

        return (2**level, 2**level,
                (2**level * self.TileSize)/(self.extent[1] - self.extent[0]),
                (2**level * self.TileSize)/(self.extent[3] - self.extent[2]))

    def ConvertGeo2View(self, lonlat, view_offset_x, view_offset_y, view_llon,
                        view_tlat, ppd_x, ppd_y):
        """Convert a geo (lon+lat) position to view pixel coords.

        lonlat  (longitude, latitude) of point

        Return screen pixels coordinates of the point (x,y).
        """

        (lon, lat) = lonlat

        lat_rad = math.radians(lat)
        n = 2.0**self.level

        x = int((lon+180.0)*256.0/360.0*n)
        y = int((1.0 - math.log(math.tan(lat_rad) +
                                (1/math.cos(lat_rad)))/math.pi)*256.0/2.0*n)

        return (x-view_offset_x, y-view_offset_y)

    def ConvertView2Geo(self, xy, view_offset_x, view_offset_y, map_llon,
                        map_tlat, ppd_x, ppd_y):
        """Convert an x,y view position to geo lon+lat.

        xy  tuple of view X & Y coordinate (pixels)

        Return a tuple (lon, lat) - geo coordinates of the point.
        """

        (x, y) = xy

        # make map-global coordinates
        x = (x+view_offset_x)/256.0
        y = (y+view_offset_y)/256.0

        # number of tiles
        n = 2.0 ** self.level

        lon_deg = 360.0*x/n - 180.0
        lat_rad = math.atan(math.sinh(math.pi * (1 - 2.0*y/n)))
        lat_deg = math.degrees(lat_rad)

        return (lon_deg, lat_deg)

    def GetTile(self, x, y):
        """Get bitmap for tile at tile coords (x, y), current level.

        x  X coord of tile required (tile coordinates)
        y  Y coord of tile required (tile coordinates)

        Look for the tile in the local cache, return if found.  If *not* local,
        start getting from servers and return 'pending' tile bitmap.
        """

        tile = self.cache.GetTile(self.level, x, y)
        if tile is None:
            tile = self.pending_tile
            self.GetInternetTile(self.level, x, y)

        return tile

    def GetInternetTile(self, level, x, y):
        """Start the process to get internet tile.

        level, x, y  identify the required tile

        If we aren't already getting this tile, queue a request and
        also put the request into a 'queued request' dictionary.  We
        do this as we can't peek into a Queue to see what's there.
        """

        tile_id = (level, x, y)
        if tile_id not in self.queued_requests:
            # add tile request to the server request queue
            self.request_queue.put(tile_id)
            self.queued_requests[tile_id] = True

    def _tile_available(self, level, x, y, image):
        """A tile is available.

        level  level for the tile
        x      x coordinate of tile
        y      y coordinate of tile
        image  tile image data
        """

        # convert image to bitmap, save in cache
        bitmap = image.ConvertToBitmap()
        self.cache.CacheTile(image, bitmap, level, x, y)

        # remove the request from the queued requests
        # note that it may not be there - a level change can flush the dict
        try:
            del self.queued_requests[(level, x, y)]
        except KeyError:
            log('deleting non-existant queued request: %d,%d,%d'
                % (level, x, y))
            pass

        # tell the world a new tile is available
        wx.CallAfter(self.available_callback, level, x, y, image, bitmap)

################################################################################
# Worker class for internet tile retrieval
################################################################################

class TileWorker(threading.Thread):
    """Thread class that gets request from a queue, loads the tile,
    and calls the callback.
    """

    def __init__(self, server, tilepath, requests, callafter, error_tile):
        """Prepare the tile worker.

        server     server URL
        tilepath   path to tile on server
        requests   the request queue
        callafter  function to CALL AFTER tile available

        Results are returned in the CallAfter() params.
        """

        threading.Thread.__init__(self)

        self.server = server
        self.tilepath = tilepath
        self.requests = requests
        self.callafter = callafter
        self.error_tile_image = error_tile
        self.daemon = True

    def run(self):
        while True:
            (level, x, y) = self.requests.get()

            try:
                tile_url = self.server + self.tilepath % (level, x, y)
                f = urllib2.urlopen(urllib2.Request(tile_url))
                if f.info().getheader('Content-Type') == 'image/jpeg':
                    image = wx.ImageFromStream(f, wx.BITMAP_TYPE_JPEG)
                else:
                    # tile not available!
                    image = self.error_tile_image
                wx.CallAfter(self.callafter, level, x, y, image)
            except urllib2.HTTPError, e:
                log('ERROR getting tile %d,%d,%d from %s\n%s'
                    % (level, x, y, tile_url, str(e)))
                pass

            self.requests.task_done()

################################################################################
# The main application frame
################################################################################

class AppFrame(wx.Frame):
    def __init__(self, tile_dir=TileDirectory):
        wx.Frame.__init__(self, None, size=DefaultAppSize,
                          title='%s %s' % (DemoName, DemoVersion))
        self.SetMinSize(DefaultAppSize)
        self.panel = wx.Panel(self, wx.ID_ANY)
        self.panel.SetBackgroundColour(wx.WHITE)
        self.panel.ClearBackground()

        self.tile_directory = tile_dir

        # build the GUI
        self.make_gui(self.panel)

        # do initialisation stuff - all the application stuff
        self.init()

        # finally, set up application window position
        self.Centre()

        # create select event dispatch directory
        self.demo_select_dispatch = {}

        # finally, bind events to handlers
        self.pyslip.Bind(pyslip.EVT_PYSLIP_SELECT, self.handle_select_event)
        self.pyslip.Bind(pyslip.EVT_PYSLIP_POSITION, self.handle_position_event)
        self.pyslip.Bind(pyslip.EVT_PYSLIP_LEVEL, self.handle_level_change)

#####
# Build the GUI
#####

    def make_gui(self, parent):
        """Create application GUI."""

        # start application layout
        all_display = wx.BoxSizer(wx.HORIZONTAL)
        parent.SetSizer(all_display)

        # put map view in left of horizontal box
        sl_box = self.make_gui_view(parent)
        all_display.Add(sl_box, proportion=1, border=1, flag=wx.EXPAND)

        # small spacer here - separate view and controls
        all_display.AddSpacer(HSpacerSize)

        # add controls to right of spacer
        controls = self.make_gui_controls(parent)
        all_display.Add(controls, proportion=0, border=1)

        parent.SetSizerAndFit(all_display)

    def make_gui_view(self, parent):
        """Build the map view widget

        parent  reference to the widget parent

        Returns the static box sizer.
        """

        # create gui objects
        sb = AppStaticBox(parent, '')
        tile_object = MyTiles(self.tile_directory, MyTiles.TileLevels,
                              pending_file='pending_tile.png',
                              error_file='error_tile.png')
        self.pyslip = pyslip.PySlip(parent, tile_src=tile_object,
                                    min_level=MinTileLevel)

        # lay out objects
        box = wx.StaticBoxSizer(sb, orient=wx.HORIZONTAL)
        box.Add(self.pyslip, proportion=1, border=1, flag=wx.EXPAND)

        return box

    def make_gui_controls(self, parent):
        """Build the 'controls' part of the GUI

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # all controls in vertical box sizer
        controls = wx.BoxSizer(wx.VERTICAL)

        # add the map level in use widget
        level = self.make_gui_level(parent)
        controls.Add(level, proportion=0, flag=wx.EXPAND|wx.ALL)

        # vertical spacer
        controls.AddSpacer(VSpacerSize)

        # add the mouse position feedback stuff
        mouse = self.make_gui_mouse(parent)
        controls.Add(mouse, proportion=0, flag=wx.EXPAND|wx.ALL)

        # vertical spacer
        controls.AddSpacer(VSpacerSize)

        # controls for map-relative points layer
        point = self.make_gui_point(parent)
        controls.Add(point, proportion=0, flag=wx.EXPAND|wx.ALL)

        # vertical spacer
        controls.AddSpacer(VSpacerSize)

        # controls for view-relative points layer
        point_view = self.make_gui_point_view(parent)
        controls.Add(point_view, proportion=0, flag=wx.EXPAND|wx.ALL)

        # vertical spacer
        controls.AddSpacer(VSpacerSize)

        # controls for map-relative image layer
        image = self.make_gui_image(parent)
        controls.Add(image, proportion=0, flag=wx.EXPAND|wx.ALL)

        # vertical spacer
        controls.AddSpacer(VSpacerSize)

        # controls for map-relative image layer
        image_view = self.make_gui_image_view(parent)
        controls.Add(image_view, proportion=0, flag=wx.EXPAND|wx.ALL)

        # vertical spacer
        controls.AddSpacer(VSpacerSize)

        # controls for map-relative text layer
        text = self.make_gui_text(parent)
        controls.Add(text, proportion=0, flag=wx.EXPAND|wx.ALL)

        # vertical spacer
        controls.AddSpacer(VSpacerSize)

        # controls for view-relative text layer
        text_view = self.make_gui_text_view(parent)
        controls.Add(text_view, proportion=0, flag=wx.EXPAND|wx.ALL)

        # vertical spacer
        controls.AddSpacer(VSpacerSize)

        # controls for map-relative polygon layer
        poly = self.make_gui_poly(parent)
        controls.Add(poly, proportion=0, flag=wx.EXPAND|wx.ALL)

        # vertical spacer
        controls.AddSpacer(VSpacerSize)

        # controls for view-relative polygon layer
        poly_view = self.make_gui_poly_view(parent)
        controls.Add(poly_view, proportion=0, flag=wx.EXPAND|wx.ALL)

        return controls

    def make_gui_level(self, parent):
        """Build the control that shows the level.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create objects
        txt = wx.StaticText(parent, wx.ID_ANY, 'Level: ')
        self.map_level = wx.StaticText(parent, wx.ID_ANY, ' ')

        # lay out the controls
        sb = AppStaticBox(parent, 'Map level')
        box = wx.StaticBoxSizer(sb, orient=wx.HORIZONTAL)
        box.Add(txt, border=PackBorder, flag=(wx.ALIGN_CENTER_VERTICAL
                                     |wx.ALIGN_RIGHT|wx.LEFT))
        box.Add(self.map_level, proportion=0, border=PackBorder,
                flag=wx.RIGHT|wx.TOP)

        return box

    def make_gui_mouse(self, parent):
        """Build the mouse part of the controls part of GUI.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create objects
        txt = wx.StaticText(parent, wx.ID_ANY, 'Lon/Lat: ')
        self.mouse_position = ROTextCtrl(parent, '', size=(150,-1),
                                         tooltip=('Shows the mouse '
                                                  'longitude and latitude '
                                                  'on the map'))

        # lay out the controls
        sb = AppStaticBox(parent, 'Mouse position')
        box = wx.StaticBoxSizer(sb, orient=wx.HORIZONTAL)
        box.Add(txt, border=PackBorder, flag=(wx.ALIGN_CENTER_VERTICAL
                                     |wx.ALIGN_RIGHT|wx.LEFT))
        box.Add(self.mouse_position, proportion=1, border=PackBorder,
                flag=wx.RIGHT|wx.TOP|wx.BOTTOM)

        return box

    def make_gui_point(self, parent):
        """Build the points part of the controls part of GUI.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create widgets
        point_obj = LayerControl(parent, 'Points, map relative %s'
                                         % str(MRPointShowLevels),
                                 selectable=True)

        # tie to event handler(s)
        point_obj.Bind(EVT_ONOFF, self.pointOnOff)
        point_obj.Bind(EVT_SHOWONOFF, self.pointShowOnOff)
        point_obj.Bind(EVT_SELECTONOFF, self.pointSelectOnOff)

        return point_obj

    def make_gui_point_view(self, parent):
        """Build the view-relative points part of the GUI.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create widgets
        point_obj = LayerControl(parent, 'Points, view relative',
                                 selectable=True)

        # tie to event handler(s)
        point_obj.Bind(EVT_ONOFF, self.pointViewOnOff)
        point_obj.Bind(EVT_SHOWONOFF, self.pointViewShowOnOff)
        point_obj.Bind(EVT_SELECTONOFF, self.pointViewSelectOnOff)

        return point_obj

    def make_gui_image(self, parent):
        """Build the image part of the controls part of GUI.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create widgets
        image_obj = LayerControl(parent, 'Images, map relative %s'
                                         % str(MRImageShowLevels),
                                 selectable=True)

        # tie to event handler(s)
        image_obj.Bind(EVT_ONOFF, self.imageOnOff)
        image_obj.Bind(EVT_SHOWONOFF, self.imageShowOnOff)
        image_obj.Bind(EVT_SELECTONOFF, self.imageSelectOnOff)

        return image_obj

    def make_gui_image_view(self, parent):
        """Build the view-relative image part of the controls part of GUI.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create widgets
        image_obj = LayerControl(parent, 'Images, view relative')

        # tie to event handler(s)
        image_obj.Bind(EVT_ONOFF, self.imageViewOnOff)
        image_obj.Bind(EVT_SHOWONOFF, self.imageViewShowOnOff)
        image_obj.Bind(EVT_SELECTONOFF, self.imageViewSelectOnOff)

        return image_obj

    def make_gui_text(self, parent):
        """Build the map-relative text part of the controls part of GUI.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create widgets
        text_obj = LayerControl(parent, 'Text, map relative %s'
                                        % str(MRTextShowLevels),
                                selectable=True, editable=True)

        # tie to event handler(s)
        text_obj.Bind(EVT_ONOFF, self.textOnOff)
        text_obj.Bind(EVT_SHOWONOFF, self.textShowOnOff)
        text_obj.Bind(EVT_SELECTONOFF, self.textSelectOnOff)

        return text_obj

    def make_gui_text_view(self, parent):
        """Build the view-relative text part of the controls part of GUI.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create widgets
        text_view_obj = LayerControl(parent, 'Text, view relative',
                                     selectable=True)

        # tie to event handler(s)
        text_view_obj.Bind(EVT_ONOFF, self.textViewOnOff)
        text_view_obj.Bind(EVT_SHOWONOFF, self.textViewShowOnOff)
        text_view_obj.Bind(EVT_SELECTONOFF, self.textViewSelectOnOff)

        return text_view_obj

    def make_gui_poly(self, parent):
        """Build the map-relative polygon part of the controls part of GUI.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create widgets
        poly_obj = LayerControl(parent,
                                'Polygon, map relative %s'
                                     % str(MRPolyShowLevels),
                                selectable=True)

        # tie to event handler(s)
        poly_obj.Bind(EVT_ONOFF, self.polyOnOff)
        poly_obj.Bind(EVT_SHOWONOFF, self.polyShowOnOff)
        poly_obj.Bind(EVT_SELECTONOFF, self.polySelectOnOff)

        return poly_obj

    def make_gui_poly_view(self, parent):
        """Build the view-relative polygon part of the controls part of GUI.

        parent  reference to parent

        Returns reference to containing sizer object.
        """

        # create widgets
        poly_view_obj = LayerControl(parent, 'Polygon, view relative',
                                     selectable=True)

        # tie to event handler(s)
        poly_view_obj.Bind(EVT_ONOFF, self.polyViewOnOff)
        poly_view_obj.Bind(EVT_SHOWONOFF, self.polyViewShowOnOff)
        poly_view_obj.Bind(EVT_SELECTONOFF, self.polyViewSelectOnOff)

        return poly_view_obj

    ######
    # pySlip demo control event handlers
    ######

##### map-relative point layer

    def pointOnOff(self, event):
        """Handle OnOff event for point layer control."""

        if event.state:
            self.point_layer = \
                self.pyslip.AddPointLayer(PointData, map_rel=True,
                                          color=PointDataColour, radius=3,
                                          offset_x=0, offset_y=0, visible=True,
                                          #show_levels=MRPointShowLevels,
                                          name='<pt_layer>')
        else:
            self.pyslip.DeleteLayer(self.point_layer)
            self.point_layer = None
            if self.sel_point_layer:
                self.pyslip.DeleteLayer(self.sel_point_layer)
                self.sel_point_layer = None
                self.sel_point = None

    def pointShowOnOff(self, event):
        """Handle ShowOnOff event for point layer control."""

        if event.state:
            self.pyslip.ShowLayer(self.point_layer)
            if self.sel_point_layer:
                self.pyslip.ShowLayer(self.sel_point_layer)
        else:
            self.pyslip.HideLayer(self.point_layer)
            if self.sel_point_layer:
                self.pyslip.HideLayer(self.sel_point_layer)

    def pointSelectOnOff(self, event):
        """Handle SelectOnOff event for point layer control."""

        layer = self.point_layer
        if event.state:
            self.add_select_handler(layer, self.pointSelect)
            self.pyslip.SetLayerSelectable(layer, True)
        else:
            self.del_select_handler(layer)
            self.pyslip.SetLayerSelectable(layer, False)

    def pointSelect(self, event):
        """Handle point select exception from pyslip.

        event  the event that contains these attributes:
                   layer_id  ID of the layer the select is for
                   sel_type  type of select event
                   point     selected point(s) geo coords+data
                                 ((x,y), data)
                             (if None then no point(s) selected)

        The point select is designed to be click for on,
        then click again for off.
        """

        if event.evtype == pyslip.EventPointSelect:
            if event.point:
                (point, data) = event.point
            if event.point is None or point == self.sel_point:
                # select again, turn point off
                self.sel_point = None
                self.pyslip.DeleteLayer(self.sel_point_layer)
                self.sel_point_layer = None
            elif point:
                if self.sel_point_layer:
                    self.pyslip.DeleteLayer(self.sel_point_layer)
                self.sel_point = point
                self.sel_point_layer = \
                    self.pyslip.AddPointLayer((point,), map_rel=True,
                                              color='#0000ff',
                                              radius=5, visible=True,
                                              #show_levels=MRPointShowLevels,
                                              name='<sel_pt_layer>')
        if event.evtype == pyslip.EventBoxSelect: # left box select
            # remove any previous selection
            if self.sel_point_layer:
                self.pyslip.DeleteLayer(self.sel_point_layer)
                self.sel_point_layer = None

            if event.point:
                pts = [pt for (pt,d) in event.point]
                self.sel_point_layer = \
                    self.pyslip.AddPointLayer(pts, map_rel=True,
                                              color='#00ffff',
                                              radius=5, visible=True,
                                              #show_levels=[3,4],
                                              name='<boxsel_pt_layer>')
                self.pyslip.PlaceLayerBelowLayer(self.sel_point_layer,
                                                 self.point_layer)

        return True

##### view-relative point layer

    def pointViewOnOff(self, event):
        """Handle OnOff event for point view layer control."""

        if event.state:
            self.point_view_layer = \
                self.pyslip.AddPointLayer(PointViewData, map_rel=False,
                                          placement='se',
                                          color=PointViewDataColour, radius=1,
                                          visible=True,
                                          name='<point_view_layer>')
        else:
            self.pyslip.DeleteLayer(self.point_view_layer)
            self.point_view_layer = None
            if self.sel_point_view_layer:
                self.pyslip.DeleteLayer(self.sel_point_view_layer)
                self.sel_point_view_layer = None
                self.sel_point_view = None

    def pointViewShowOnOff(self, event):
        """Handle ShowOnOff event for point view layer control."""

        if event.state:
            self.pyslip.ShowLayer(self.point_view_layer)
            if self.sel_point_view_layer:
                self.pyslip.ShowLayer(self.sel_point_view_layer)
        else:
            self.pyslip.HideLayer(self.point_view_layer)
            if self.sel_point_view_layer:
                self.pyslip.HideLayer(self.sel_point_view_layer)

    def pointViewSelectOnOff(self, event):
        """Handle SelectOnOff event for point view layer control."""

        layer = self.point_view_layer
        if event.state:
            self.add_select_handler(layer, self.pointViewSelect)
            self.pyslip.SetLayerSelectable(layer, True)
        else:
            self.del_select_handler(layer)
            self.pyslip.SetLayerSelectable(layer, False)

    def pointViewSelect(self, event):
        """Handle view-relative point select event from pyslip.

        event  the event that contains these attributes:
                   layer_id  ID of the layer the select is for
                   sel_type  type of select event
                   point     selected point(s) geo coordinates
                             (if None then no point(s) was selected)

        The point select is designed to be click for on,
        then click again for off.
        """


        if event.evtype == pyslip.EventPointSelect:
            if event.point:
                (point, data) = event.point
            if event.point is None or point == self.sel_point_view:
                # select again, turn point off
                self.sel_point_view = None
                self.pyslip.DeleteLayer(self.sel_point_view_layer)
                self.sel_point_view_layer = None
            elif event.point:
                if self.sel_point_view_layer:
                    self.pyslip.DeleteLayer(self.sel_point_view_layer)
                self.sel_point_view = point
                self.sel_point_view_layer = \
                    self.pyslip.AddPointLayer((point,), map_rel=False,
                                              color='#0000ff',
                                              radius=3, visible=True,
                                              name='<sel_pt_view_layer>')
        elif event.evtype == pyslip.EventBoxSelect:
            # remove any previous selection
            if self.sel_point_view_layer:
                self.pyslip.DeleteLayer(self.sel_point_view_layer)
                self.sel_point_view_layer = None

            if event.point:
                pts = [pt for (pt,d) in event.point]
                self.sel_point_view_layer = \
                    self.pyslip.AddPointLayer(pts, map_rel=False,
                                              color='#00ffff',
                                              radius=3, visible=True,
                                              name='<boxsel_pt_view_layer>')
                self.pyslip.PlaceLayerBelowLayer(self.sel_point_view_layer,
                                                 self.point_view_layer)

        return True

##### map-relative image layer

    def imageOnOff(self, event):
        """Handle OnOff event for map-relative image layer control."""

        if event.state:
            self.image_layer = \
                self.pyslip.AddImageLayer(ImageData, map_rel=True,
                                          visible=True,
                                          #show_levels=MRImageShowLevels,
                                          name='<image_layer>')
        else:
            self.pyslip.DeleteLayer(self.image_layer)
            self.image_layer = None
            if self.sel_image_layer:
                self.pyslip.DeleteLayer(self.sel_image_layer)
                self.sel_image_layer = None
                self.sel_image = None

    def imageShowOnOff(self, event):
        """Handle ShowOnOff event for image layer control."""

        if event.state:
            self.pyslip.ShowLayer(self.image_layer)
            if self.sel_image_layer:
                self.pyslip.ShowLayer(self.sel_image_layer)
        else:
            self.pyslip.HideLayer(self.image_layer)
            if self.sel_image_layer:
                self.pyslip.HideLayer(self.sel_image_layer)

    def imageSelectOnOff(self, event):
        """Handle SelectOnOff event for image layer control."""

        layer = self.image_layer
        if event.state:
            self.add_select_handler(layer, self.imageSelect)
            self.pyslip.SetLayerSelectable(layer, True)
        else:
            self.del_select_handler(layer)
            self.pyslip.SetLayerSelectable(layer, False)

    def imageSelect(self, event):
        """Select event from pyslip."""

        point = event.point

        if event.evtype == pyslip.EventPointSelect:
            if point == self.sel_image:
                # select again, turn point off
                self.sel_image = None
                self.pyslip.DeleteLayer(self.sel_image_layer)
                self.sel_image_layer = None
            elif point:
                if self.sel_image_layer:
                    self.pyslip.DeleteLayer(self.sel_image_layer)
                self.sel_image = point
                self.sel_image_layer = \
                    self.pyslip.AddPointLayer((point,), map_rel=True,
                                              color='#0000ff',
                                              radius=5, visible=True,
                                              #show_levels=[3,4],
                                              name='<sel_pt_layer>')
        elif event.evtype == pyslip.EventBoxSelect:
            # remove any previous selection
            if self.sel_image_layer:
                self.pyslip.DeleteLayer(self.sel_image_layer)
                self.sel_image_layer = None

            if point:
                self.sel_image_layer = \
                    self.pyslip.AddPointLayer(point, map_rel=True,
                                              color='#00ffff',
                                              radius=5, visible=True,
                                              #show_levels=[3,4],
                                              name='<boxsel_pt_layer>')
                self.pyslip.PlaceLayerBelowLayer(self.sel_image_layer,
                                                 self.image_layer)

        return True

    def imageBSelect(self, id, points=None):
        """Select event from pyslip."""

        # remove any previous selection
        if self.sel_image_layer:
            self.pyslip.DeleteLayer(self.sel_image_layer)
            self.sel_image_layer = None

        if points:
            self.sel_image_layer = \
                self.pyslip.AddPointLayer(points, map_rel=True,
                                          color='#e0e0e0',
                                          radius=13, visible=True,
                                          #show_levels=[3,4],
                                          name='<boxsel_img_layer>')
            self.pyslip.PlaceLayerBelowLayer(self.sel_image_layer,
                                             self.image_layer)

        return True

##### view-relative image layer

    def imageViewOnOff(self, event):
        """Handle OnOff event for view-relative image layer control."""

        if event.state:
            self.image_view_layer = \
                self.pyslip.AddImageLayer(ImageViewData, map_rel=False,
                                          visible=True,
                                          name='<image_view_layer>')
        else:
            self.pyslip.DeleteLayer(self.image_view_layer)
            self.image_view_layer = None
            if self.sel_image_view_layer:
                self.pyslip.DeleteLayer(self.sel_image_view_layer)
                self.sel_image_view_layer = None
                self.sel_image_view_point = None

    def imageViewShowOnOff(self, event):
        """Handle ShowOnOff event for image layer control."""

        if event.state:
            self.pyslip.ShowLayer(self.image_view_layer)
            if self.sel_image_view_layer:
                self.pyslip.ShowLayer(self.sel_image_layer)
        else:
            self.pyslip.HideLayer(self.image_view_layer)
            if self.sel_image_view_layer:
                self.pyslip.HideLayer(self.sel_image_layer)

    def imageViewSelectOnOff(self, event):
        """Handle SelectOnOff event for image layer control."""

        layer = self.image_view_layer
        if event.state:
            self.add_select_handler(layer, self.imageViewSelect)
            self.pyslip.SetLayerSelectable(layer, True)
        else:
            self.del_select_handler(layer)
            self.pyslip.SetLayerSelectable(layer, False)

    def imageViewSelect(self, id, posn=None):
        """View-relative image select event from pyslip."""

        if posn:
            for p in ImageViewData:
                pp = (p[0], p[1])
                if pp == posn:
                    if pp == self.sel_image_view:
                        # select again, turn point off
                        self.sel_image_view = None
                        self.pyslip.DeleteLayer(self.sel_image_view_layer)
                        self.sel_image_view_layer = None
                    else:
                        if self.sel_image_view_layer:
                            self.pyslip.DeleteLayer(self.sel_image_view_layer)
                        self.sel_image_view = pp
                        self.sel_image_view_layer = \
                            self.pyslip.AddPointLayer((pp,), map_rel=False,
                                                      color='#00ffff',
                                                      radius=5, visible=True,
                                                      name='<sel_image_view>')
        return True

##### map-relative text layer

    def textOnOff(self, event):
        """Handle OnOff event for map-relative text layer control."""

        if event.state:
            self.text_layer = \
                self.pyslip.AddTextLayer(TextData, map_rel=True,
                                         name='<text_layer>', visible=True,
                                         #show_levels=MRTextShowLevels,
                                         placement='ne')
        else:
            self.pyslip.DeleteLayer(self.text_layer)
            self.text_layer = None
            if self.sel_text_layer:
                self.pyslip.DeleteLayer(self.sel_text_layer)
                self.sel_text_layer = None
                self.sel_text_point = None

    def textShowOnOff(self, event):
        """Handle ShowOnOff event for text layer control."""

        if event.state:
            self.pyslip.ShowLayer(self.text_layer)
            if self.sel_text_layer:
                self.pyslip.ShowLayer(self.sel_text_layer)
        else:
            self.pyslip.HideLayer(self.text_layer)
            if self.sel_text_layer:
                self.pyslip.HideLayer(self.sel_text_layer)

    def textSelectOnOff(self, event):
        """Handle SelectOnOff event for text layer control."""

        layer = self.text_layer
        if event.state:
            self.add_select_handler(layer, self.textSelect)
            self.pyslip.SetLayerSelectable(layer, True)
        else:
            self.del_select_handler(layer)
            self.pyslip.SetLayerSelectable(layer, False)


    def textSelect(self, event):
        """Map-relative text select event from pyslip."""

        if event.evtype == pyslip.EventPointSelect:
            if event.point:
                (point, data) = event.point
            if event.point is None or point == self.sel_text:
                # select again, turn point off
                self.sel_text = None
                self.pyslip.DeleteLayer(self.sel_text_layer)
                self.sel_text_layer = None
            elif point:
                if self.sel_text_layer:
                    self.pyslip.DeleteLayer(self.sel_text_layer)
                self.sel_text = point
                self.sel_text_layer = \
                    self.pyslip.AddPointLayer((point,), map_rel=True,
                                              color='#0000ff',
                                              radius=5, visible=True,
                                              #show_levels=MRTextShowLevels,
                                              name='<sel_text_layer>')
                self.pyslip.PlaceLayerBelowLayer(self.sel_text_layer,
                                                 self.text_layer)
        if event.evtype == pyslip.EventBoxSelect: # left box select
            # remove any previous selection
            if self.sel_text_layer:
                self.pyslip.DeleteLayer(self.sel_text_layer)
                self.sel_text_layer = None

            if event.point:
                pts = [pt for (pt,d) in event.point]
                self.sel_text_layer = \
                    self.pyslip.AddPointLayer(pts, map_rel=True,
                                              color='#00ffff',
                                              radius=5, visible=True,
                                              #show_levels=[3,4],
                                              name='<boxsel_text_layer>')
                self.pyslip.PlaceLayerBelowLayer(self.sel_text_layer,
                                                 self.text_layer)

        return True

##### view-relative text layer

    def textViewOnOff(self, event):
        """Handle OnOff event for map-relative text layer control."""

        if event.state:
            self.text_view_layer = \
                self.pyslip.AddTextLayer(TextViewData, map_rel=False,
                                         name='<text_view_layer>',
                                         placement='cn', visible=True,
                                         fontsize=24, textcolor='#0000ff')
        else:
            self.pyslip.DeleteLayer(self.text_view_layer)
            self.text_view_layer = None
            if self.sel_text_view_layer:
                self.pyslip.DeleteLayer(self.sel_text_view_layer)
                self.sel_text_view_layer = None
                self.sel_text_view_point = None

    def textViewShowOnOff(self, event):
        """Handle ShowOnOff event for text layer control."""

        if event.state:
            self.pyslip.ShowLayer(self.text_view_layer)
            if self.sel_text_view_layer:
                self.pyslip.ShowLayer(self.sel_text_view_layer)
        else:
            self.pyslip.HideLayer(self.text_view_layer)
            if self.sel_text_view_layer:
                self.pyslip.HideLayer(self.sel_text_view_layer)

    def textViewSelectOnOff(self, event):
        """Handle SelectOnOff event for text layer control."""

        layer = self.text_view_layer
        if event.state:
            self.add_select_handler(layer, self.textViewSelect)
            self.pyslip.SetLayerSelectable(layer, True)
        else:
            self.del_select_handler(layer)
            self.pyslip.SetLayerSelectable(layer, False)

    def textViewSelect(self, event):
        """Map-relative text select event from pyslip."""

#        if posn:
#            for p in TextData:
#                pp = (p[0], p[1])
#                if pp == posn:
#                    if pp == self.sel_view_text:
#                        # select again, turn point off
#                        self.sel_view_text = None
#                        self.pyslip.DeleteLayer(self.sel_text_view_layer)
#                        self.sel_text_view_layer = None
#                    else:
#                        if self.sel_text_view_layer:
#                            self.pyslip.DeleteLayer(self.sel_text_view_layer)
#                        self.sel_view_text = pp
#                        self.sel_text_view_layer = \
#                            self.pyslip.AddPointLayer((pp,), map_rel=True,
#                                                      color='#80ffff',
#                                                      radius=5, visible=True,
#                                                      name='<sel_text>')
#        return True

        if event.evtype == pyslip.EventPointSelect:
            if event.point:
                (point, data) = event.point
            if event.point is None or point == self.sel_text_view:
                # select again, turn point off
                self.sel_text_view = None
                self.pyslip.DeleteLayer(self.sel_text_view_layer)
                self.sel_text_view_layer = None
            elif point:
                if self.sel_text_view_layer:
                    self.pyslip.DeleteLayer(self.sel_text_view_layer)
                self.sel_text_view = point
                self.sel_text_view_layer = \
                    self.pyslip.AddPointLayer((point,), map_rel=True,
                                              color='#80ffff',
                                              radius=5, visible=True,
                                              #show_levels=MRTextShowLevels,
                                              name='<sel_text_view_layer>')
        elif event.evtype == pyslip.EventRightPointSelect:    # right point select
            pass
        if event.evtype == pyslip.EventBoxSelect: # left box select
            # remove any previous selection
            if self.sel_text_view_layer:
                self.pyslip.DeleteLayer(self.sel_text_view_layer)
                self.sel_text_view_layer = None

            if event.point:
                pts = [pt for (pt,d) in event.point]
                self.sel_text_view_layer = \
                    self.pyslip.AddPointLayer(pts, map_rel=True,
                                              color='#00ffff',
                                              radius=5, visible=True,
                                              #show_levels=[3,4],
                                              name='<boxsel_text_view_layer>')
                self.pyslip.PlaceLayerBelowLayer(self.sel_text_view_layer,
                                                 self.text_view_layer)
        else:   # right box select
            pass

        return True

##### map-relative polygon layer

    def polyOnOff(self, event):
        """Handle OnOff event for map-relative polygon layer control."""

        if event.state:
            self.poly_layer = \
                self.pyslip.AddPolygonLayer(PolyData, map_rel=True,
                                            visible=True,
                                            #show_levels=MRPolyShowLevels,
                                            name='<poly_layer>')
        else:
            self.pyslip.DeleteLayer(self.poly_layer)
            self.poly_layer = None
            if self.sel_poly_layer:
                self.pyslip.DeleteLayer(self.sel_poly_layer)
                self.sel_poly_layer = None
                self.sel_poly_point = None

    def polyShowOnOff(self, event):
        """Handle ShowOnOff event for polygon layer control."""

        if event.state:
            self.pyslip.ShowLayer(self.poly_layer)
            if self.sel_poly_layer:
                self.pyslip.ShowLayer(self.sel_poly_layer)
        else:
            self.pyslip.HideLayer(self.poly_layer)
            if self.sel_poly_layer:
                self.pyslip.HideLayer(self.sel_poly_layer)

    def polySelectOnOff(self, event):
        """Handle SelectOnOff event for polygon layer control."""

        layer = self.poly_layer
        if event.state:
            self.add_select_handler(layer, self.polySelect)
            self.pyslip.SetLayerSelectable(layer, True)
        else:
            self.del_select_handler(layer)
            self.pyslip.SetLayerSelectable(layer, False)

    def polySelect(self, event):
        """Map-relative polygon select event from pyslip.

        event  the pyslip event which has attributes:
                   evtype    the event type
                   layer_id  ID of the layer selected
                   point     iterable of poly points, can be None
                   mposn     map-relative position of mouse-click
                   vposn     view-relative position of mouse-click
        """

        poly = event.point

        if event.evtype == pyslip.EventPointSelect:
            if poly:
                if poly == self.sel_poly:
                    # polygon selected again, turn selection off
                    self.sel_poly = None
                    self.pyslip.DeleteLayer(self.sel_poly_layer)
                    self.sel_poly_layer = None
                else:
                    # new selection
                    if self.sel_poly_layer:
                        # deselect previously selected poly
                        self.pyslip.DeleteLayer(self.sel_poly_layer)
                    self.sel_poly = poly
                    self.sel_poly_layer = \
                        self.pyslip.AddPointLayer(poly, map_rel=True,
                                                  color='#ff00ff',
                                                  radius=9, visible=True,
                                                  #show_levels=[3,4],
                                                  name='<sel_poly>')
        else:   # box select, not yet implemented
            pass

        return True

##### view-relative polygon layer

    def polyViewOnOff(self, event):
        """Handle OnOff event for map-relative polygon layer control."""

        if event.state:
            self.poly_view_layer = \
                self.pyslip.AddPolygonLayer(PolyViewData, map_rel=False,
                                            name='<poly_view_layer>',
                                            placement='cn', visible=True,
                                            fontsize=24, color='#0000ff')
        else:
            self.pyslip.DeleteLayer(self.poly_view_layer)
            self.poly_view_layer = None
            if self.sel_poly_view_layer:
                self.pyslip.DeleteLayer(self.sel_poly_view_layer)
                self.sel_poly_view_layer = None
                self.sel_poly_view_point = None

    def polyViewShowOnOff(self, event):
        """Handle ShowOnOff event for polygon layer control."""

        if event.state:
            self.pyslip.ShowLayer(self.poly_view_layer)
            if self.sel_poly_view_layer:
                self.pyslip.ShowLayer(self.sel_poly_view_layer)
        else:
            self.pyslip.HideLayer(self.poly_view_layer)
            if self.sel_poly_view_layer:
                self.pyslip.HideLayer(self.sel_poly_view_layer)

    def polyViewSelectOnOff(self, event):
        """Handle SelectOnOff event for polygon layer control."""

        layer = self.poly_view_layer
        if event.state:
            self.add_select_handler(layer, self.polyViewSelect)
            self.pyslip.SetLayerSelectable(layer, True)
        else:
            self.del_select_handler(layer)
            self.pyslip.SetLayerSelectable(layer, False)

    def polyViewSelect(self, id, posn=None):
        """View-relative polygon select event from pyslip."""

        if posn:
            for p in PolyData:
                pp = (p[0], p[1])
                if pp == posn:
                    if pp == self.sel_view_poly:
                        # select again, turn polygon off
                        self.view_sel_poly = None
                        self.pyslip.DeleteLayer(self.sel_poly_view_layer)
                        self.sel_poly_view_layer = None
                    else:
                        if self.sel_poly_view_layer:
                            self.pyslip.DeleteLayer(self.sel_poly_view_layer)
                        self.view_sel_poly = pp
                        self.sel_poly_view_layer = \
                            self.pyslip.AddPointLayer((pp,), map_rel=True,
                                                      color='#80ffff',
                                                      radius=5, visible=True,
                                                      name='<sel_polyv>')
        return True

    ######
    # Finish initialization of data, etc
    ######

    def init(self):
        global PointData, PointDataColour
        global PointViewData, PointViewDataColour
        global ImageData
        global ImageViewData
        global TextData # , TextDataColour
        global TextViewData
        global PolyData
        global PolyViewData

        # create PointData
        PointData = []
        count = 0
        for lon in range(-70, 290+1, 5):
            for lat in range(-65, 65+1, 5):
                PointData.append((lon, lat, {'data': count}))
                count += 1
        PointDataColour = '#ff000080'	# semi-transparent

        # create PointViewData - a point-rendition of 'PYSLIP'
        PointViewData = [(-66,-14),(-66,-13),(-66,-12),(-66,-11),(-66,-10),
                         (-66,-9),(-66,-8),(-66,-7),(-66,-6),(-66,-5),(-66,-4),
                         (-66,-3),(-65,-7),(-64,-7),(-63,-7),(-62,-7),(-61,-8),
                         (-60,-9),(-60,-10),(-60,-11),(-60,-12),(-61,-13),
                         (-62,-14),(-63,-14),(-64,-14),(65,-14),            # P
                         (-59,-14),(-58,-13),(-57,-12),(-56,-11),(-55,-10),
                         (-53,-10),(-52,-11),(-51,-12),(-50,-13),(-49,-14),
                         (-54,-9),(-54,-8),(-54,-7),(-54,-6),(-54,-5),
                         (-54,-4),(-54,-3),                                 # Y
                         (-41,-13),(-42,-14),(-43,-14),(-44,-14),(-45,-14),
                         (-46,-14),(-47,-13),(-48,-12),(-48,-11),(-47,-10),
                         (-46,-9),(-45,-9),(-44,-9),(-43,-9),(-42,-8),
                         (-41,-7),(-41,-6),(-41,-5),(-42,-4),(-43,-3),
                         (-44,-3),(-45,-3),(-46,-3),(-47,-3),(-48,-4),       # S
                         (-39,-14),(-39,-13),(-39,-12),(-39,-11),(-39,-10),
                         (-39,-9),(-39,-8),(-39,-7),(-39,-6),(-39,-5),
                         (-39,-4),(-39,-3),(-38,-3),(-37,-3),(-36,-3),
                         (-35,-3),(-34,-3),(-33,-3),(-32,-3),                # L
                         (-29,-14),(-29,-13),(-29,-12),
                         (-29,-11),(-29,-10),(-29,-9),(-29,-8),(-29,-7),
                         (-29,-6),(-29,-5),(-29,-4),(-29,-3),                # I
                         (-26,-14),(-26,-13),(-26,-12),(-26,-11),(-26,-10),
                         (-26,-9),(-26,-8),(-26,-7),(-26,-6),(-26,-5),(-26,-4),
                         (-26,-3),(-25,-7),(-24,-7),(-23,-7),(-22,-7),(-21,-8),
                         (-20,-9),(-20,-10),(-20,-11),(-20,-12),(-21,-13),
                         (-22,-14),(-23,-14),(-24,-14),(25,-14)]             # P
        PointViewDataColour = '#00ff0020'	# very transparent

        # create image data
        ImageData = [# Agnes Napier - 1855
                     (160.0, -30.0, ShipImg, {'placement': 'cc'}),
                     # Venus - 1826
                     (145.0, -11.0, ShipImg, {'placement': 'ne'}),
                     # Wolverine - 1879
                     (156.0, -23.0, ShipImg, {'placement': 'nw'}),
                     # Thomas Day - 1884
                     (150.0, -15.0, ShipImg, {'placement': 'sw'}),
                     # Sybil - 1902
                     (165.0, -19.0, ShipImg, {'placement': 'se'}),
                     # Prince of Denmark - 1863
                     (158.55, -19.98, ShipImg),
                     # Moltke - 1911
                     (146.867525, -19.152185, ShipImg)
                    ]
        ImageData2 = []
        ImageData3 = []
        ImageData4 = []
        ImageData5 = []
        ImageData6 = []
        self.map_level_2_img = {0: ImageData2,
                                1: ImageData3,
                                2: ImageData4,
                                3: ImageData5,
                                4: ImageData6}
        self.map_level_2_selimg = {0: SelGlassyImg2,
                                   1: SelGlassyImg3,
                                   2: SelGlassyImg4,
                                   3: SelGlassyImg5,
                                   4: SelGlassyImg6}
        self.current_layer_img_layer = None
        for x in range(80):
            for y in range(40):
                ImageData.append((-30+x*2, y*2-30, GlassyImg4))

        ImageViewData = [(0, 0, CompassRoseGraphic, {'placement': 'ne'})]

        text_placement = {'placement': 'se'}
        transparent_placement = {'placement': 'se', 'colour': '#00000040'}
        capital = {'placement': 'se', 'fontsize': 14, 'color': 'red',
                   'textcolour': 'red'}
        TextData = [(151.216, -33.855, 'Sydney', text_placement),
                    (144.963, -37.814, 'Melbourne', {'placement': 'ce'}),
                    (153.024, -27.469, 'Brisbane', text_placement),
                    (115.860, -31.953, 'Perth', transparent_placement),
                    (138.600, -34.929, 'Adelaide', text_placement),
                    (130.841, -12.460, 'Darwin', text_placement),
                    (147.328, -42.883, 'Hobart', text_placement),
                    (174.765, -36.854, 'Auckland', text_placement),
                    (174.781, -41.287, 'Wellington', capital),
                    (172.637, -43.531, 'Christchurch', text_placement),
                    (168.661, -45.032, 'Queenstown', text_placement),
                    (147.150, -09.479, 'Port Moresby', capital),
                    (106.828, -06.181, 'Jakarta', capital),
                    (110.374, -07.785, 'Yogyakarta', text_placement),
                    (120.980, +14.591, 'Manila', capital),
                    (-88.244, +40.117, 'Champaign', text_placement),
                    (160.000, -30.000, 'Agnes Napier - 1855',
                        {'placement': 'cw', 'offset_x': 20, 'color': 'green'}),
                    (145.000, -11.000, 'Venus - 1826',
                        {'placement': 'sw', 'color': 'green'}),
                    (156.000, -23.000, 'Wolverine - 1879',
                        {'placement': 'ce', 'color': 'green'}),
                    (150.000, -15.000, 'Thomas Day - 1884',
                        {'color': 'green'}),
                    (165.000, -19.000, 'Sybil - 1902',
                        {'placement': 'cw', 'color': 'green'}),
                    (158.550, -19.980, 'Prince of Denmark - 1863',
                        {'placement': 'nw', 'offset_x': 20, 'color': 'green'}),
                    (146.867525, -19.152182, 'Moltke - 1911',
                        {'placement': 'ce', 'offset_x': 20, 'color': 'green'})
                   ]
        if sys.platform != 'win32':
            TextData.extend([
                    (106.367, +10.360, 'Mỹ Tho', {'placement': 'ne'}),
                    (105.852, +21.029, 'Hà Nội', capital),
                    (106.701, +10.785, 'Thành phố Hồ Chí Minh', {'placement': 'sw'}),
                    (132.452, +34.392, '広島市 (Hiroshima City)', text_placement),
                    (114.167, +22.264, '香港 (Hong Kong)', {'placement': 'nw'}),
                    ( 96.150, +16.798, 'ရန်ကုန် (Rangoon)', capital),
                    (104.933, +11.563, ' ភ្នំពេញ (Phnom Penh)', 
                        {'placement': 'ce', 'fontsize': 12, 'color': 'red'}),
                    (100.496, +13.760, 'กรุงเทพมหานคร (Bangkok)', capital),
                    ( 77.582, +34.163, 'གླེ་(Leh)', text_placement),
                    ( 84.991, +24.697, 'बोधगया (Bodh Gaya)', text_placement)])
#        TextDataColour = '#ffffff40'

        TextViewData = [(0, 7, '%s %s' % (DemoName, DemoVersion))]

        PolyData = [(((150,10),(160,20),(170,10),(165,0),(155,0)),
                      {'width': 3, 'color': 'blue', 'closed': True}),
                    (((165,-35),(175,-35),(175,-45),(165,-45)),
                      {'width': 10, 'color': '#00ff00c0', 'filled': True,
                       'fillcolor': '#ffff0040'}),
                    (((190,-30),(220,-50),(220,-30),(190,-50)),
                      {'width': 3, 'color': 'green', 'filled': True,
                       'fillcolor': 'yellow'}),
                    (((170,+50),(220,+65),(220,+50),(170,+65)),
                      {'width': 10, 'color': '#00000040'})
                   ]

        PolyViewData = [(((0,0),(230,0),(230,40),(-230,40),(-230,0)),
                        {'width': 3, 'color': '#00ff00ff', 'closed': True,
                         'placement': 'cn', 'offset_y': 1})]

        # set initial view position
        self.pyslip.GotoLevelAndPosition(InitViewLevel, InitViewPosition)
        self.map_level.SetLabel('%d' % InitViewLevel)

        # define layer ID variables & sub-checkbox state variables
        self.point_layer = None
        self.sel_point_layer = None
        self.sel_point = None

        self.point_view_layer = None
        self.sel_point_view_layer = None
        self.sel_point_view = None

        self.image_layer = None
        self.sel_image_layer = None
        self.sel_image = None

        self.image_view_layer = None
        self.sel_image_view_layer = None
        self.sel_image_view = None

        self.text_layer = None
        self.sel_text_layer = None
        self.sel_text = None

        self.text_view_layer = None
        self.sel_text_view_layer = None
        self.sel_view_text = None

        self.poly_layer = None
        self.sel_poly_layer = None
        self.sel_poly = None

        self.poly_view_layer = None
        self.sel_poly_view_layer = None
        self.sel_poly = None

        # force pyslip initialisation
        self.pyslip.OnSize()

    ######
    # Exception handlers
    ######

    def handle_select_event(self, event):
        """Handle a pySlip point/box SELECT event."""

        layer_id = event.layer_id
        #point = event.point

        self.demo_select_dispatch.get(layer_id, self.null_handler)(event)

    def null_handler(self, event):
        """Routine to handle unexpected events."""

        print('ERROR: null_handler!?')

    def handle_position_event(self, event):
        """Handle a pySlip POSITION event."""

        posn_str = ''
        if event.position:
            (lon, lat) = event.position
            posn_str = ('%.*f / %.*f'
                        % (LonLatPrecision, lon, LonLatPrecision, lat))

        self.mouse_position.SetValue(posn_str)

    def handle_level_change(self, event):
        """Handle a pySlip LEVEL event."""

        self.map_level.SetLabel('%d' % event.level)

    ######
    # Handle adding/removing select handler functions.
    ######

    def add_select_handler(self, id, handler):
        """Add handler for select in layer 'id'."""

        self.demo_select_dispatch[id] = handler

    def del_select_handler(self, id):
        """Remove handler for select in layer 'id'."""

        del self.demo_select_dispatch[id]

################################################################################

if __name__ == '__main__':
    import sys
    import traceback
    import tkinter_error

    # our own handler for uncaught exceptions
    def excepthook(type, value, tb):
        msg = '\n' + '=' * 80
        msg += '\nUncaught exception:\n'
        msg += ''.join(traceback.format_exception(type, value, tb))
        msg += '=' * 80 + '\n'
        log(msg)
        tkinter_error.tkinter_error(msg)
        sys.exit(1)

    # use user tile directory, if supplied
    tile_dir = None
    if len(sys.argv) > 1:
        tile_dir = sys.argv[1]

    # plug our handler into the python system
    sys.excepthook = excepthook

    # start wxPython app
    app = wx.App()
    if tile_dir:
        app_frame = AppFrame(tile_dir=tile_dir)
    else:
        app_frame = AppFrame()
    app_frame.Show()
    
##    import wx.lib.inspection
##    wx.lib.inspection.InspectionTool().Show()

    app.MainLoop()

